#include "config.h"
#include "func_map.h"
#include "raw_mode.h"
#include "key_process.h"

#define TEMP_DIR 			JOKY_HOME
#define TEMP_FILE_PREFIX 	".joky_temp_"
#define TEMP_FILE_TEMPLATE 	(TEMP_DIR TEMP_FILE_PREFIX "XXXXXX")

/* loop the whole program until user type "exit" to exit */
void loop();
/* read user input from the terminal(stdin) and return a string */
char *read_input();
/* split the input string to tokens and return a list of tokens */
char **split_input(const char *line);
/* execute the command */
int execute(char **args);
/* record history command */
void record_history_cmd(char *line);
/* count the number of temp file for keep trace of the running subshell */
int count_tmp_file();

int history_fd; 		/* the file descriptor of history cmd file */
char *prompt; 			/* prompt */
char *dir;  			/* current directory */
pid_t child_pid; 		/* child process id */
extern char **environ; 	/* environment array */

int main()
{
	/* create the temp file */
    char temp_file[] = TEMP_FILE_TEMPLATE;
    int temp_fd = mkstemp(temp_file); 
    if (temp_fd == -1) {
		fprintf(stderr, "joky error: main-->mkstemp\n");
		return -1;
    }

	/* init the screen */
	char *cls_argv[] = {"clear", NULL}; _clear(cls_argv);
	char *hi_argv[] = {"hi", "joky", NULL};
	_hi(hi_argv);

	/* create a history file to store cmd */
	history_fd = open(CMD_HST_FILE, O_CREAT | O_APPEND | O_RDWR, 0644);
	if (history_fd == -1) {
		fprintf(stderr, "joky error: open for history_file\n");
		return -1;
	}

	/* loop */
	enable_raw_mode();	
	loop();
	disable_raw_mode();	
	/* end loop and delete the history file */
	if (count_tmp_file() == 1)
		unlink(CMD_HST_FILE);

	unlink(temp_file);
	close(temp_fd);
	close(history_fd);

	return 0;
}

void loop()
{
	char *line; 		/* store the input string */
	char **tokens; 		/* store the split tokens */
	int status; 		/* status of executing command */

	do {
		prompt = get_prompt();
		dir = cur_dir();
		if (prompt == NULL || dir == NULL) {
			disable_raw_mode();
			exit(EXIT_FAILURE);
		}
		printf("%s %s", prompt, dir);
		fflush(stdout);

		line = read_input();
		//printf("%sRead %ld bytes successfully!---str:%s%s\n", YELLOW, strlen(line), line, RESET);
		record_history_cmd(line);
		tokens = split_input(line);

		//for (char **tp = tokens; *tp != NULL; tp++)
			//printf("%stoken: %s%s\n", RED, *tp, RESET);

		status = execute(tokens);

		/* free the requested memory */
		free(dir);
		dir = NULL;
		free(prompt);
		prompt = NULL;
		free(line);
		line = NULL;
		for (char **tp = tokens; *tp != NULL; tp++)
			free(*tp);
		free(tokens);
		tokens = NULL;
	} while (status != 1);
}

char *read_input()
{
	/* allocate memory for storing input string */
	int buf_size = CMD_LINE_BUFSIZE;

	char *buf = (char*) malloc(sizeof(char) * buf_size);
	if (!buf) {
		fprintf(stderr, "%sjoky: Allocation error for read_input!%s\n", RED, RESET);
		return NULL;
	}

	int c; 
	int position = 0;

	while (1) {
		int end;
		do {
			end = 0;
			/* read input from terminal */
			c = getchar();

        	end = key_process(c, buf, &position, prompt, dir);
		} while (end != 1);

        if (c == EOF || c == '\n' || c == '\r') {
            buf[position] = '\0';
            return buf;
        }

		/* reallocate memory for buf */
		if (position >= buf_size) {
			buf_size *= 2;
			buf = (char*) realloc(buf, buf_size);

			if (!buf) {
				fprintf(stderr, "%sdash: Reallocation error for read_input%s\n", RED, RESET);
				return NULL;
			}
		}
	}

	return buf;
} 

/* assume using space to seperate the parameters */
char **split_input(const char *line)
{
	/* check if the param is valid */
	if (!line) {
		fprintf(stderr, "%sjoky: Failed in spliting the cmd-line%s\n", RED, RESET);
		return NULL;
	}

	/* allocate memory for storing the tokens */
	char **buf = (char **) malloc(sizeof(char *) * CMD_TOKEN_INIT_SIZE);
	if (!buf) {
		fprintf(stderr, "%sjoky: Allocation error for split_input-->buf\n%s", RED, RESET);
		return NULL;
	}

	int buf_size = CMD_TOKEN_INIT_SIZE;
	int buf_count = 0;

	const char *ptr = line; 	/* locate the token's first char that we are handling */
	while (*ptr != '\0') {
		/* skip the space */
		while (*ptr == ' ' || *ptr == '\t') {
			ptr++;
		}
		if (*ptr == '\0') {
			break;
		}

		/* reallocation for the buf */
		if (buf_count >= buf_size) {
			buf_size *= 2;
			if (buf_size > CMD_TOKEN_MAX_SIZE) buf_size = CMD_TOKEN_MAX_SIZE;
			char **new_buf = (char **) realloc(buf, sizeof(char *) * buf_size);
			if (!new_buf) {
				fprintf(stderr, "%sjoky: Reallocation error for split_input-->buf%s\n", RED, RESET);
				/* clean up previous allocated memory */
				for (int i = 0; i < buf_count; i++) {
					free(buf[i]);
				}
				free(buf);
				return NULL;
			}
			buf = new_buf;
		}

		/* calculate the token's length */
		const char *tmp = ptr;
		while (*tmp != ' ' && *tmp != '\t' && *tmp != '\0')
			tmp++;
		int len = tmp - ptr;

		/* allocate memory for this token */
		buf[buf_count] = (char *) malloc(sizeof(char) * (len + 1));
		if (!buf[buf_count]) {
			fprintf(stderr, "%sjoky: Allocation error for split_input-->token%s\n", RED, RESET);
			/* clean up previous allocated memory */
			for (int i = 0; i < buf_count; i++) {
				free(buf[i]);
			}
			free(buf);
			return NULL;
		}

		/* copy the token string to the buf */
		strncpy(buf[buf_count], ptr, len);
		buf[buf_count][len] = '\0';
		buf_count++;

		ptr = tmp;
	}

	buf[buf_count] = NULL; 		/* the last buf is NULL, which means the end of the token list */

	return buf;
}

/* find program function with the same name to execute */
func_ptr find_func(const char *name)
{
	for (int i = 0; func_map[i].name != NULL; i++) {
		if (strcmp(func_map[i].name, name) == 0)
			return func_map[i].func;
	}
	return NULL;
}

/* handle the sigint */
void safe_write(const char *msg) {
	// use write instead of printf because printf is non-reentrant
	write(STDOUT_FILENO, msg, strlen(msg));
}

void signal_handler(int signo) {
    switch (signo) {
        case SIGINT:
            safe_write("\n" UNDERLINE "Received SIGINT (Ctrl+C)" RESET "\n");
            break;
        case SIGTERM:
            safe_write("\n" UNDERLINE "Received SIGTERM" RESET "\n");
            break;
        case SIGHUP:
            safe_write("\n" UNDERLINE "Received SIGHUP." RESET "\n");
            break;
        case SIGQUIT:
            safe_write("\n" UNDERLINE "Received SIGQUIT (Ctrl+\\)." RESET "\n");
            break;
        case SIGTSTP:
            safe_write("\n" UNDERLINE "Received SIGTSTP (Ctrl+Z)." RESET "\n");
            break;
        default:
            {
                char buffer[50];
                snprintf(buffer, sizeof(buffer), "\n" UNDERLINE "Received signal %d." RESET "\n", signo);
                safe_write(buffer);
            }
            break;
    }

    enable_raw_mode();
    _exit(0);
}
/* handle redirection */
int output_redirection(const char *output, int is_append)
{
	int open_flags;
	if (is_append == 1) {
		open_flags = O_WRONLY | O_CREAT | O_APPEND;
	} else {
		open_flags = O_WRONLY | O_CREAT | O_TRUNC;
	}

	int fd = open(output, open_flags, 0644);
	if (fd == -1) {
		fprintf(stderr, "joky error: output redirection\n");
		return -1;
	}
	dup2(fd, STDOUT_FILENO); // Redirect stdout to file
	close(fd);

	return 0;
}
int input_redirection(const char *input)
{
	int fd = open(input, O_RDONLY);
	if (fd == -1) {
		fprintf(stderr, "joky error: input redirection\n");
		return -1;
	}
	dup2(fd, STDIN_FILENO); // Redirect stdin to file
	close(fd);

	return 0;
}

/* failure: -1, exit: 1, normal: 0 */
int execute(char **args)
{
	/* check if the params is not null */
	if (args == NULL || args[0] == NULL) {
		//fprintf(stderr, "%sjoky: No command provided%s\n", RED, RESET);
		return -1;
	}

	/* exit joky normally */
	if (strcmp(args[0], "exit") == 0) {
		// printf("%sExit normally...%s\n", MAGENTA, RESET);
		exit_prog_bar();
		return 1;
	}

	// check if execute a subshell
	int subshell = 0;
	if (strcmp(args[0], "joky") == 0) {
		subshell = 1;
	}

	/* find function through first arg(program name) */
	func_ptr prog = find_func(args[0]);
	if (subshell != 1 && prog == NULL) {
		fprintf(stderr, "%sjoky: Could not find command >>> %s'%s'%s\n", RED, GREEN_HL, args[0], RESET);
		return -1;
	}

	/* exectute that function */
	int status = -1;

	if (strcmp(args[0], "cd") == 0) {
		status = prog(args);
		return status;
	}

	// Variables for redirection and pipe
	char *input_file = NULL;
	char *output_file = NULL;
#ifdef PIPE
	int pipe_fd[2];
	int prev_pipe_fd[2] = {-1, -1};
#endif

	// Count the number of cmd seperated by '|'
	int cmd_count = 0;
	for (int i = 0; args[i] != NULL; i++) {
		if (strcmp(args[i], "|") == 0)
			cmd_count++;
	}
	cmd_count++; // Last command after last pipe

	/* execute command by order, which is seperated by "|" */
	int cmd_start = 0; // index for start of executing command
	for (int cmd_index = 0; cmd_index < cmd_count; cmd_index++) {
#ifdef PIPE
		// create a new pipe for the current command
		if (cmd_index < cmd_count - 1) { // the last command do not need
			if (pipe(pipe_fd) == -1) {
				fprintf(stderr, "joky error: create a pipe for cmd-%d\n", cmd_index + 1);
				return -1;
			}
		}
#endif

        int arg_index = 0; // arg index for every command
		// Prepare args for the current command
		char *current_cmd[CMD_TOKEN_MAX_SIZE]; 			// Assuming max 10 args per command
		for (int i = cmd_start; args[i] != NULL && strcmp(args[i], "|") != 0; i++) {
			current_cmd[arg_index] = strdup(args[i]);
			// printf("token: %s\n", current_cmd[arg_index]);
			arg_index++;
		}
		current_cmd[arg_index] = NULL; 	// Null-terminate current command args
		
		// fork a new process
		child_pid = fork();
		if (child_pid == -1) {
			fprintf(stderr, "joky error: create a process\n");
			// release memory for current_cmd
			for (int i = 0; i < arg_index; i++) {
				free(current_cmd[i]);
			}
			return -1;
		} else if (child_pid == 0) {
			/* child process */
			signal(SIGINT, signal_handler);
			signal(SIGTERM, signal_handler);
			signal(SIGQUIT, signal_handler);
			signal(SIGHUP, signal_handler);
			signal(SIGTSTP, signal_handler);
#ifdef PIPE
            if (cmd_index > 0) {
                // If not the first command, set input to the previous pipe
                dup2(prev_pipe_fd[0], STDIN_FILENO);
                close(prev_pipe_fd[0]);
            }
            if (cmd_index < cmd_count - 1) {
                // If not the last command, set output to the current pipe
                dup2(pipe_fd[1], STDOUT_FILENO);
                close(pipe_fd[1]);
            }
#endif

			/* redirection - start */
			int is_append = 0;
			for (int i = 0; current_cmd[i] != NULL; i++) {
				if (strcmp(current_cmd[i], "<") == 0) {
					input_file = current_cmd[i + 1];
					current_cmd[i] = NULL; // Null-terminate current_cmd array
				} else if (strcmp(current_cmd[i], ">") == 0) {
					output_file = current_cmd[i + 1];
					current_cmd[i] = NULL; // Null-terminate current_cmd array
				} else if (strcmp(current_cmd[i], ">>") == 0) {
					is_append = 1;
					output_file = current_cmd[i + 1];
					current_cmd[i] = NULL; // Null-terminate current_cmd array
				}
			}
			// handle output redirection
			if (output_file != NULL) {
				if (output_redirection(output_file, is_append) == -1) {
					// release memory for current_cmd
					for (int i = 0; i < arg_index; i++) {
						free(current_cmd[i]);
					}
					return -1;
				}
			}
			// handle input redirection
			if (input_file != NULL) {
				if (input_redirection(input_file) == -1) {
					// release memory for current_cmd
					for (int i = 0; i < arg_index; i++) {
						free(current_cmd[i]);
					}
					return -1;
				}
			}
			/* redirection - end */

#ifdef TRACE
			for (int i = 0; current_cmd[i] != NULL; i++) {
				printf("current_cmd[%d]: %s\n", i, current_cmd[i]);
			}
#endif

			/* execute command */
			disable_raw_mode();
			if (subshell) {
				// return(main) the same to exit
				execve("joky", current_cmd, environ);
			} else {
				status = prog(current_cmd);
				enable_raw_mode();
				exit(status);
			}
		} else {
			/* parent process */
			signal(SIGINT, SIG_IGN);
			signal(SIGTERM, SIG_IGN);
			signal(SIGQUIT, SIG_IGN);
			signal(SIGHUP, SIG_IGN);
			signal(SIGTSTP, SIG_IGN);
#ifdef PIPE
            if (cmd_index > 0) { // the first command do not need close
                close(prev_pipe_fd[0]); // Close previous pipe read end
            }
            if (cmd_index < cmd_count - 1) { // the last command do not need save for next command
                prev_pipe_fd[0] = pipe_fd[0]; // Save current pipe read end for next command
                close(pipe_fd[1]); // Close current pipe write end
            }
#endif
			waitpid(child_pid, &status, 0);
			enable_raw_mode();
		}

		// release memory for current_cmd
		for (int i = 0; i < arg_index; i++) {
			free(current_cmd[i]);
		}

		// Update the start index for the next command
        cmd_start += arg_index; 
        if (args[cmd_start] != NULL && strcmp(args[cmd_start], "|") == 0) {
            cmd_start++; // Skip the pipe symbol
        }
	}

	return status;
}

void record_history_cmd(char *line)
{
	write(history_fd, line, strlen(line));
	write(history_fd, "\n", 1);
}

int count_tmp_file()
{
	int count = 0;
    struct dirent *entry;
    DIR *dp = opendir(TEMP_DIR);

    if (dp == NULL) {
		fprintf(stderr, "joky error: count_tmp_file-->opendir\n");
        return -1;
    }

    while ((entry = readdir(dp))) {
        if (strncmp(entry->d_name, TEMP_FILE_PREFIX, strlen(TEMP_FILE_PREFIX)) == 0) {
            count++;
        }
    }

    closedir(dp);
    return count;
}
